/**
 * Copyright (c) 2011, OPOWER.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 1) Redistributions of source code must retain the above
 * copyright notice, this list of conditions and the following disclaimer.
 * 2) Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution. 3) Neither the name of the OPOWER.com nor
 * the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.opower.connectionpool;

// JDBC JDK
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

// collections management
import java.util.ArrayList;
import java.util.Collection;

// logging
import org.apache.log4j.Logger;

/**
 * Simple implementation of {@link ConnectionPool}. The class is NOT
 * public because it should not be accessed directly from outside
 * of the package. Instead, clients should use {@link PoolFactory}
 * in order to get an access to a new pool. For example:
 *
 * <pre>
 * {@code
 * import com.opower.connectionpool;
 * final ConnectionPool pool = new PoolFactory().getPool();
 * }
 * </pre>
 *
 * @author Yegor Bugayenko (yegor@tpc2.com)
 * @version $Id: SimplePool.java 7 2011-04-23 15:59:25Z yegor256@gmail.com $
 */
final class SimplePool implements ConnectionPool {

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(SimplePool.class);

    /**
     * Maximum number of connections to keep open
     * in pool. When the total is higher than this
     * we will try to reduce the number of open connections.
     * @see #setThresholds(Integer,Integer)
     */
    private Integer maximum = null;

    /**
     * Minimum number of connections to keep open
     * in pool. When the total is lower than this
     * we will try to increase the number of open connections.
     * @see #setThresholds(Integer,Integer)
     */
    private Integer minimum = null;

    /**
     * JDBC URL.
     */
    private final String url;

    /**
     * User name.
     */
    private final String user;

    /**
     * Password.
     */
    private final String password;

    /**
     * List of available connections.
     */
    private final Collection<Connection> available =
        new ArrayList<Connection>();

    /**
     * List of busy connections.
     */
    private final Collection<Connection> busy =
        new ArrayList<Connection>();

    /**
     * Public ctor.
     * @param addr JDBC URL of the driver end-point
     * @param usr User name
     * @param pwd The password
     * @see java.sql.DriverManager#getConnection(String,String,String)
     * @see SimplePool
     */
    public SimplePool(final String addr, final String usr, final String pwd) {
        this.LOGGER.trace("#SimplePool(...)");
        this.url = addr;
        this.user = usr;
        this.password = pwd;
        this.LOGGER.debug("Pool created: " + addr);
    }

    /**
     * Configure the pool with thresholds.
     * @param min The minimum
     * @param max The maximum
     * @see #optimize()
     * @throws SQLException If something goes wrong
     */
    public void setThresholds(final Integer min, final Integer max)
        throws SQLException {
        this.minimum = min;
        this.maximum = max;
        this.optimize();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Connection getConnection() throws SQLException {
        this.LOGGER.trace("#getConnection()");
        Connection connection = null;
        synchronized (this) {
            if (this.available.isEmpty()) {
                // there is nothing to use, let's create a new one
                this.available.add(this.connection());
            }
            for (Connection conn : this.available) {
                connection = conn;
                break;
            }
            this.available.remove(connection);
            this.busy.add(connection);
        }
        this.optimize();
        this.LOGGER.debug(
            "Connection '"
            + connection
            + "' successfully locked at "
            + this
        );
        return connection;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void releaseConnection(final Connection connection)
        throws SQLException {
        this.LOGGER.trace("#releaseConnection(...)");
        if (!this.busy.contains(connection)) {
            throw new IllegalArgumentException(
                "Connection is not in pool: " + connection
            );
        }
        synchronized (this) {
            this.busy.remove(connection);
            this.available.add(connection);
        }
        this.optimize();
        this.LOGGER.debug(
            "Connection '"
            + connection
            + "' successfully released at "
            + this
        );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        this.LOGGER.trace("#toString()");
        return String.format(
            "%s[%d/%d]",
            this.getClass().getName(),
            this.busy.size(),
            this.busy.size() + this.available.size()
        );
    }

    /**
     * Instantiate new connection and return it.
     * @return The connection instantiated
     * @throws SQLException If something goes wrong
     */
    private Connection connection() throws SQLException {
        final Connection connection =
            DriverManager.getConnection(this.url, this.user, this.password);
        this.LOGGER.info("Connection instantiated: " + this.url);
        return connection;
    }

    /**
     * Optimize the pool.
     * @see #getConnection()
     * @see #releaseConnection(Connection)
     * @throws SQLException If something goes wrong
     */
    private void optimize() throws SQLException {
        this.LOGGER.trace("#optimize()");
        synchronized (this) {
            final Connection[] array =
                this.available.toArray(new Connection[] {});
            final Integer total = this.available.size() + this.busy.size();
            if (this.maximum != null && total > this.maximum) {
                final Integer toDel = total - this.busy.size() - this.maximum;
                for (int idx = 0; idx < toDel; idx += 1) {
                    this.available.remove(array[idx]);
                    this.LOGGER.debug("Pool shrinked: " + this);
                }
            }
            if (this.minimum != null && total < this.minimum) {
                final Integer toAdd = this.minimum - total - this.busy.size();
                for (int idx = 0; idx < toAdd; idx += 1) {
                    this.available.add(this.connection());
                    this.LOGGER.debug("Pool extended: " + this);
                }
            }
        }
    }

}
